/*
 * Copyright (C) 2008 Esmertec AG.
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.mms.model;

import com.android.mms.ContentRestrictionException;
import com.android.mms.dom.smil.parser.SmilXmlSerializer;
import com.android.mms.drm.DrmWrapper;
import com.android.mms.layout.LayoutManager;
import com.google.android.mms.ContentType;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.GenericPdu;
import com.google.android.mms.pdu.MultimediaMessagePdu;
import com.google.android.mms.pdu.PduBody;
import com.google.android.mms.pdu.PduHeaders;
import com.google.android.mms.pdu.PduPart;
import com.google.android.mms.pdu.PduPersister;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILElement;
import org.w3c.dom.smil.SMILLayoutElement;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILParElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.w3c.dom.smil.SMILRootLayoutElement;
import android.content.ContentResolver;
import android.content.Context;
import android.drm.mobile1.DrmException;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

public class SlideshowModel extends Model implements List<SlideModel>, IModelChangedObserver {
	private static final String TAG = "SlideshowModel";
	private final LayoutModel mLayout;
	private final ArrayList<SlideModel> mSlides;
	private SMILDocument mDocumentCache;
	private PduBody mPduBodyCache;
	private int mCurrentMessageSize;
	private ContentResolver mContentResolver;

	private SlideshowModel(ContentResolver contentResolver) {
		mLayout = new LayoutModel();
		mSlides = new ArrayList<SlideModel>();
		mContentResolver = contentResolver;
	}

	private SlideshowModel(LayoutModel layouts, ArrayList<SlideModel> slides, SMILDocument documentCache,
			PduBody pbCache, ContentResolver contentResolver) {
		mLayout = layouts;
		mSlides = slides;
		mContentResolver = contentResolver;
		mDocumentCache = documentCache;
		mPduBodyCache = pbCache;
		for (SlideModel slide : mSlides) {
			increaseMessageSize(slide.getSlideSize());
			slide.setParent(this);
		}
	}

	public static SlideshowModel createNew(Context context) {
		return new SlideshowModel(context.getContentResolver());
	}

	public static SlideshowModel createFromMessageUri(Context context, Uri uri) throws MmsException {
		return createFromPduBody(context, getPduBody(context, uri));
	}

	public static SlideshowModel createFromPduBody(Context context, PduBody pb) throws MmsException {
		SMILDocument document = SmilHelper.getDocument(pb);
		// Create root-layout model.
		SMILLayoutElement sle = document.getLayout();
		SMILRootLayoutElement srle = sle.getRootLayout();
		int w = srle.getWidth();
		int h = srle.getHeight();
		if ((w == 0) || (h == 0)) {
			w = LayoutManager.getInstance().getLayoutParameters().getWidth();
			h = LayoutManager.getInstance().getLayoutParameters().getHeight();
			srle.setWidth(w);
			srle.setHeight(h);
		}
		RegionModel rootLayout = new RegionModel(null, 0, 0, w, h);
		// Create region models.
		ArrayList<RegionModel> regions = new ArrayList<RegionModel>();
		NodeList nlRegions = sle.getRegions();
		int regionsNum = nlRegions.getLength();
		for (int i = 0; i < regionsNum; i++) {
			SMILRegionElement sre = (SMILRegionElement) nlRegions.item(i);
			RegionModel r = new RegionModel(sre.getId(), sre.getFit(), sre.getLeft(), sre.getTop(), sre.getWidth(),
					sre.getHeight(), sre.getBackgroundColor());
			regions.add(r);
		}
		LayoutModel layouts = new LayoutModel(rootLayout, regions);
		// Create slide models.
		SMILElement docBody = document.getBody();
		NodeList slideNodes = docBody.getChildNodes();
		int slidesNum = slideNodes.getLength();
		ArrayList<SlideModel> slides = new ArrayList<SlideModel>(slidesNum);
		for (int i = 0; i < slidesNum; i++) {
			// FIXME: This is NOT compatible with the SMILDocument which is
			// generated by some other mobile phones.
			SMILParElement par = (SMILParElement) slideNodes.item(i);
			// Create media models for each slide.
			NodeList mediaNodes = par.getChildNodes();
			int mediaNum = mediaNodes.getLength();
			ArrayList<MediaModel> mediaSet = new ArrayList<MediaModel>(mediaNum);
			for (int j = 0; j < mediaNum; j++) {
				SMILMediaElement sme = (SMILMediaElement) mediaNodes.item(j);
				try {
					MediaModel media = MediaModelFactory.getMediaModel(context, sme, layouts, pb);
					SmilHelper.addMediaElementEventListeners((EventTarget) sme, media);
					mediaSet.add(media);
				} catch (DrmException e) {
					Log.e(TAG, e.getMessage(), e);
				} catch (IOException e) {
					Log.e(TAG, e.getMessage(), e);
				} catch (IllegalArgumentException e) {
					Log.e(TAG, e.getMessage(), e);
				}
			}
			SlideModel slide = new SlideModel((int) (par.getDur() * 1000), mediaSet);
			slide.setFill(par.getFill());
			SmilHelper.addParElementEventListeners((EventTarget) par, slide);
			slides.add(slide);
		}
		SlideshowModel slideshow = new SlideshowModel(layouts, slides, document, pb, context.getContentResolver());
		slideshow.registerModelChangedObserver(slideshow);
		return slideshow;
	}

	public PduBody toPduBody() {
		if (mPduBodyCache == null) {
			mDocumentCache = SmilHelper.getDocument(this);
			mPduBodyCache = makePduBody(mDocumentCache);
		}
		return mPduBodyCache;
	}

	private PduBody makePduBody(SMILDocument document) {
		return makePduBody(null, document, false);
	}

	private PduBody makePduBody(Context context, SMILDocument document, boolean isMakingCopy) {
		PduBody pb = new PduBody();
		boolean hasForwardLock = false;
		for (SlideModel slide : mSlides) {
			for (MediaModel media : slide) {
				if (isMakingCopy) {
					if (media.isDrmProtected() && !media.isAllowedToForward()) {
						hasForwardLock = true;
						continue;
					}
				}
				PduPart part = new PduPart();
				if (media.isText()) {
					TextModel text = (TextModel) media;
					// Don't create empty text part.
					if (TextUtils.isEmpty(text.getText())) {
						continue;
					}
					// Set Charset if it's a text media.
					part.setCharset(text.getCharset());
				}
				// Set Content-Type.
				part.setContentType(media.getContentType().getBytes());
				String src = media.getSrc();
				String location;
				boolean startWithContentId = src.startsWith("cid:");
				if (startWithContentId) {
					location = src.substring("cid:".length());
				} else {
					location = src;
				}
				// Set Content-Location.
				part.setContentLocation(location.getBytes());
				// Set Content-Id.
				if (startWithContentId) {
					// Keep the original Content-Id.
					part.setContentId(location.getBytes());
				} else {
					int index = location.lastIndexOf(".");
					String contentId = (index == -1) ? location : location.substring(0, index);
					part.setContentId(contentId.getBytes());
				}
				if (media.isDrmProtected()) {
					DrmWrapper wrapper = media.getDrmObject();
					part.setDataUri(wrapper.getOriginalUri());
					part.setData(wrapper.getOriginalData());
				} else if (media.isText()) {
					part.setData(((TextModel) media).getText().getBytes());
				} else if (media.isImage() || media.isVideo() || media.isAudio()) {
					part.setDataUri(media.getUri());
				} else {
					Log.w(TAG, "Unsupport media: " + media);
				}
				pb.addPart(part);
			}
		}
		if (hasForwardLock && isMakingCopy && context != null) {
			Toast.makeText(context, "cannot_forward_drm_obj", Toast.LENGTH_LONG).show();
			document = SmilHelper.getDocument(pb);
		}
		// Create and insert SMIL part(as the first part) into the PduBody.
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		SmilXmlSerializer.serialize(document, out);
		PduPart smilPart = new PduPart();
		smilPart.setContentId("smil".getBytes());
		smilPart.setContentLocation("smil.xml".getBytes());
		smilPart.setContentType(ContentType.APP_SMIL.getBytes());
		smilPart.setData(out.toByteArray());
		pb.addPart(0, smilPart);
		return pb;
	}

	public PduBody makeCopy(Context context) {
		return makePduBody(context, SmilHelper.getDocument(this), true);
	}

	public SMILDocument toSmilDocument() {
		if (mDocumentCache == null) {
			mDocumentCache = SmilHelper.getDocument(this);
		}
		return mDocumentCache;
	}

	public static PduBody getPduBody(Context context, Uri msg) throws MmsException {
		PduPersister p = PduPersister.getPduPersister(context);
		GenericPdu pdu = p.load(msg);
		int msgType = pdu.getMessageType();
		if ((msgType == PduHeaders.MESSAGE_TYPE_SEND_REQ) || (msgType == PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF)) {
			return ((MultimediaMessagePdu) pdu).getBody();
		} else {
			throw new MmsException();
		}
	}

	public void setCurrentMessageSize(int size) {
		mCurrentMessageSize = size;
	}

	public int getCurrentMessageSize() {
		return mCurrentMessageSize;
	}

	public void increaseMessageSize(int increaseSize) {
		if (increaseSize > 0) {
			mCurrentMessageSize += increaseSize;
		}
	}

	public void decreaseMessageSize(int decreaseSize) {
		if (decreaseSize > 0) {
			mCurrentMessageSize -= decreaseSize;
		}
	}

	public LayoutModel getLayout() {
		return mLayout;
	}

	//
	// Implement List<E> interface.
	//
	public boolean add(SlideModel object) {
		int increaseSize = object.getSlideSize();
		checkMessageSize(increaseSize);
		if ((object != null) && mSlides.add(object)) {
			increaseMessageSize(increaseSize);
			object.registerModelChangedObserver(this);
			for (IModelChangedObserver observer : mModelChangedObservers) {
				object.registerModelChangedObserver(observer);
			}
			notifyModelChanged(true);
			return true;
		}
		return false;
	}

	public boolean addAll(Collection<? extends SlideModel> collection) {
		throw new UnsupportedOperationException("Operation not supported.");
	}

	public void clear() {
		if (mSlides.size() > 0) {
			for (SlideModel slide : mSlides) {
				slide.unregisterModelChangedObserver(this);
				for (IModelChangedObserver observer : mModelChangedObservers) {
					slide.unregisterModelChangedObserver(observer);
				}
			}
			mCurrentMessageSize = 0;
			mSlides.clear();
			notifyModelChanged(true);
		}
	}

	public boolean contains(Object object) {
		return mSlides.contains(object);
	}

	public boolean containsAll(Collection<?> collection) {
		return mSlides.containsAll(collection);
	}

	public boolean isEmpty() {
		return mSlides.isEmpty();
	}

	public Iterator<SlideModel> iterator() {
		return mSlides.iterator();
	}

	public boolean remove(Object object) {
		if ((object != null) && mSlides.remove(object)) {
			SlideModel slide = (SlideModel) object;
			decreaseMessageSize(slide.getSlideSize());
			slide.unregisterAllModelChangedObservers();
			notifyModelChanged(true);
			return true;
		}
		return false;
	}

	public boolean removeAll(Collection<?> collection) {
		throw new UnsupportedOperationException("Operation not supported.");
	}

	public boolean retainAll(Collection<?> collection) {
		throw new UnsupportedOperationException("Operation not supported.");
	}

	public int size() {
		return mSlides.size();
	}

	public Object[] toArray() {
		return mSlides.toArray();
	}

	public <T> T[] toArray(T[] array) {
		return mSlides.toArray(array);
	}

	public void add(int location, SlideModel object) {
		if (object != null) {
			int increaseSize = object.getSlideSize();
			checkMessageSize(increaseSize);
			mSlides.add(location, object);
			increaseMessageSize(increaseSize);
			object.registerModelChangedObserver(this);
			for (IModelChangedObserver observer : mModelChangedObservers) {
				object.registerModelChangedObserver(observer);
			}
			notifyModelChanged(true);
		}
	}

	public boolean addAll(int location, Collection<? extends SlideModel> collection) {
		throw new UnsupportedOperationException("Operation not supported.");
	}

	public SlideModel get(int location) {
		return mSlides.get(location);
	}

	public int indexOf(Object object) {
		return mSlides.indexOf(object);
	}

	public int lastIndexOf(Object object) {
		return mSlides.lastIndexOf(object);
	}

	public ListIterator<SlideModel> listIterator() {
		return mSlides.listIterator();
	}

	public ListIterator<SlideModel> listIterator(int location) {
		return mSlides.listIterator(location);
	}

	public SlideModel remove(int location) {
		SlideModel slide = mSlides.remove(location);
		if (slide != null) {
			decreaseMessageSize(slide.getSlideSize());
			slide.unregisterAllModelChangedObservers();
			notifyModelChanged(true);
		}
		return slide;
	}

	public SlideModel set(int location, SlideModel object) {
		SlideModel slide = mSlides.get(location);
		if (null != object) {
			int removeSize = 0;
			int addSize = object.getSlideSize();
			if (null != slide) {
				removeSize = slide.getSlideSize();
			}
			if (addSize > removeSize) {
				checkMessageSize(addSize - removeSize);
				increaseMessageSize(addSize - removeSize);
			} else {
				decreaseMessageSize(removeSize - addSize);
			}
		}
		slide = mSlides.set(location, object);
		if (slide != null) {
			slide.unregisterAllModelChangedObservers();
		}
		if (object != null) {
			object.registerModelChangedObserver(this);
			for (IModelChangedObserver observer : mModelChangedObservers) {
				object.registerModelChangedObserver(observer);
			}
		}
		notifyModelChanged(true);
		return slide;
	}

	public List<SlideModel> subList(int start, int end) {
		return mSlides.subList(start, end);
	}

	@Override
	protected void registerModelChangedObserverInDescendants(IModelChangedObserver observer) {
		mLayout.registerModelChangedObserver(observer);
		for (SlideModel slide : mSlides) {
			slide.registerModelChangedObserver(observer);
		}
	}

	@Override
	protected void unregisterModelChangedObserverInDescendants(IModelChangedObserver observer) {
		mLayout.unregisterModelChangedObserver(observer);
		for (SlideModel slide : mSlides) {
			slide.unregisterModelChangedObserver(observer);
		}
	}

	@Override
	protected void unregisterAllModelChangedObserversInDescendants() {
		mLayout.unregisterAllModelChangedObservers();
		for (SlideModel slide : mSlides) {
			slide.unregisterAllModelChangedObservers();
		}
	}

	public void onModelChanged(Model model, boolean dataChanged) {
		if (dataChanged) {
			mDocumentCache = null;
			mPduBodyCache = null;
		}
	}

	public void sync(PduBody pb) {
		for (SlideModel slide : mSlides) {
			for (MediaModel media : slide) {
				PduPart part = pb.getPartByContentLocation(media.getSrc());
				if (part != null) {
					media.setUri(part.getDataUri());
				}
			}
		}
	}

	public void checkMessageSize(int increaseSize) throws ContentRestrictionException {
		ContentRestriction cr = ContentRestrictionFactory.getContentRestriction();
		cr.checkMessageSize(mCurrentMessageSize, increaseSize, mContentResolver);
	}

	/**
	 * Determines whether this is a "simple" slideshow. Criteria: - Exactly one
	 * slide - Exactly one multimedia attachment, but no audio - It can
	 * optionally have a caption
	 */
	public boolean isSimple() {
		// There must be one (and only one) slide.
		if (size() != 1)
			return false;

		SlideModel slide = get(0);
		// The slide must have either an image or video, but not both.
		if (!(slide.hasImage() ^ slide.hasVideo()))
			return false;

		// No audio allowed.
		if (slide.hasAudio())
			return false;

		return true;
	}

	/**
	 * Make sure the text in slide 0 is no longer holding onto a reference to
	 * the text in the message text box.
	 */
	public void prepareForSend() {
		if (size() == 1) {
			TextModel text = get(0).getText();
			if (text != null) {
				text.cloneText();
			}
		}
	}
}